"use strict";

// according to html5 specification, void element may emit the / for self closing tag
var voidElements = {
    'br': 1,
};

var noop = function () {
};

var State = {
    None: 0,
    TagBegin: 1,
    TagName: 2,
    PropBegin: 3,
    PropName: 4,
    PropNameBeforeEqual: 5,
    PropEqual: 6,
    PropValue: 7,
    Text: 8,
};

var isWhitespace = (function () {
    var re = /^[ \s\r\n]$/;
    return function (c) {
        return !!c.match(re);
    }
}());

function mergeOption(o1, o2) {
    if (!o2) {
        return o1;
    }
    Object.keys(o2).forEach(function (key) {
        if (key in o2) {
            o1[key] = o2[key];
        }
    });
    return o1;
}

function HtmlParser(options) {
    var defaultOptions = {
        startDocument: noop,
        startElement: noop,
        characters: noop,
        endElement: noop,
        endDocument: noop
    };

    this.options = mergeOption(defaultOptions, options);
}

HtmlParser.prototype.parseString = function (s, offset) {
    var pos = offset;
    if (s[pos] !== '"') {
        return -1;
    }

    for (pos = offset + 1; pos < s.length; pos++) {
        if (s[pos] === '"') {
            return pos - offset + 1;
        }
    }
    return -1;
}

HtmlParser.prototype.parseComment = function (s, offset) {
    //begin with <!--
    if (s[offset] !== '<' || s[offset + 1] !== '!' || s[offset + 2] !== '-' || s[offset + 3] !== '-') {
        return -1;
    }
    var len = s.length - 2;
    for (var pos = offset + 3; pos < len; pos++) {
        if (s[pos] === '-' && s[pos + 1] === '-' && s[pos + 2] === '>') {
            return pos + 3 - offset;
        }
    }
    return -1;
}

/*

* */
HtmlParser.prototype.parse = function (s) {

    var option = this.options;
    var pos = 0;
    var state = State.None;
    var text = null;
    var tagName = null;
    var attrs = [];
    var attrName = null;
    var attrValue = null;
    var tagStack = [];

    var notifyText = function () {
        option.characters(text);
        text = null;
    };

    var onParseTag = function (selfClosing) {

        var startTag = false;
        var closeTag = false;
        if (tagName[0] === '/') {
            closeTag = true;
            tagName = tagName.substr(1);
        }
        else {
            startTag = true;
            closeTag = selfClosing === true;
        }

        if (startTag) {
            tagStack.push(tagName);
            option.startElement(tagName, attrs);
        }
        if (closeTag) {
            var tag = tagStack[tagStack.length - 1];
            if (tag !== tagName) {
                var msg = "invalid closing tag '" + tagName + "' while parsing tag '" + tag + "'";
                console.log(msg)
                throw new SyntaxError(msg);
            }
            tagStack.pop();
            option.endElement(tagName);
        }
        tagName = null;
        attrs = [];
    };

    var peek = function (index) {
        if (index >= s.length) {
            return '\0';
        }
        return s[index];
    };

    var throwParseError = function (msg) {
        console.warn(msg);
        throw new Error(msg);
    };


    option.startDocument();

    for (; pos < s.length; ++pos) {
        var c = s[pos];
        if (state === State.None) {
            if (c === '<') {
                state = State.TagBegin;
            }
            else if (isWhitespace(c)) {
                continue;
            }
            else {
                // text node
                state = State.Text;
                text = c;
            }
        }
        else if (state === State.Text) {
            if (c === '<') {
                text = text.trim();
                notifyText();
                state = State.TagBegin;
            }
            else {
                text += c;
            }
        }
        else if (state === State.TagBegin) {
            if (isWhitespace(c)) {
                throwParseError("invalid tag name");
            }
            else if (c === '!') {
                if (peek(pos + 1) === '-' && peek(pos + 2) === '-') {
                    //comment
                    var commentLen = this.parseComment(s, pos - 1);
                    pos += commentLen - 1;
                    state = State.None;
                }
                else {
                    throwParseError("invalid tag type");
                }
            }
            else {
                tagName = c;
                state = State.TagName;
            }
        }
        else if (state === State.TagName) {
            if (isWhitespace(c)) {
                state = State.PropBegin;
                attrName = "";
            }
            else if (c === '/') {
                if (peek(pos + 1) === '>') {
                    //tag end
                    onParseTag(true);
                    pos += 1;
                    state = State.None;
                }
                else {
                    throwParseError("invalid '/' while parsing tag '" + tagName + "'");
                }
            }
            else if (c === '>') {
                onParseTag(false);
                tagName = null;
                state = State.None;
            }
            else {
                tagName += c;
            }
        }
        else if (state === State.PropBegin) {
            if (isWhitespace(c)) {
                continue;
            }
            else if (c === '/') {
                if (peek(pos + 1) === '>') {
                    //tag end
                    onParseTag(true);
                    pos += 1;
                    state = State.None;
                }
                else {
                    var msg = "unexpected char '/' while parsing properties";
                    console.warn(msg);
                    throw new Error(msg);
                }
            }
            else if (c === '>') {
                onParseTag(false);
                state = State.None;
            }
            else {
                attrName = c;
                state = State.PropName;
            }
        }
        else if (state === State.PropName) {
            if (isWhitespace(c)) {
                state = State.PropNameBeforeEqual;
            }
            else if (c === '=') {
                state = State.PropEqual;
            }
            else if (c === '>') {
                attrs.push({name: attrName, value: ""});
                onParseTag(false);
                state = State.None;
            }
            else {
                attrName += c;
            }
        }
        else if (state === State.PropNameBeforeEqual) {
            if (isWhitespace(c)) {
                continue;
            }
            else if (c === '=') {
                state = State.PropEqual;
            }
            else if (c === '>') {
                attrs.push({name: attrName, value: ""});
                onParseTag(false);
            }
            else {
                attrs.push({name: attrName, value: ""});
                //prevous prop does not have value, start next prop
                state = State.PropName;
                attrName = c;
                attrValue = null;
            }
        }
        else if (state === State.PropEqual) {
            if (isWhitespace(c)) {
                continue;
            }
            else if (c === '=') {
                throwParseError("unexpected '=' in prop value ");
            }
            else if (c === '"') {
                var propLen = this.parseString(s, pos);
                if (propLen < 0) {
                    throwParseError("expect '=' for prop value);");
                }
                attrValue = s.substr(pos + 1, propLen - 2);
                attrs.push({name: attrName, value: attrValue});
                attrName = null;
                attrValue = null;

                pos += propLen - 1;
                state = State.PropBegin;
            }
        }
    }

    option.endDocument();

    return true;
}

export default HtmlParser;
export {HtmlParser};


